﻿
namespace Patterns4Net.ArchitectureExplorer.ViewModels
{
    using System.ComponentModel.Composition;
    using System.Diagnostics.Contracts;
    using Caliburn.Micro;
    using Graphviz4Net.Graphs;
    using Diagram;
    using Graph;
    using Messages;
    using Core;
    using Infrastructure;

    [Export(typeof(DiagramViewModel))]
    public class DiagramViewModel : 
        PropertyChangedBase, 
        IHandle<ZoomToDiagramElementMessage>,
        IHandle<SelectDiagramElementMessage>,
        IHandle<DiagramUpdatedMessage>,
        IHandle<StartNewDiagramMessage>,
        IHandle<ZoomToCurrentElementMessage>,
        IHandle<ZoomOutMessage>,
        IHandle<ZoomToTopLevelMessage>,
        IHandle<GoBackMessage>,
        IHandle<GoForwardMessage>
    {
        private readonly IGraphViewModelFactory graphViewModelFactory;

        private readonly Buffer<DiagramElement> goBackBuffer = new Buffer<DiagramElement>();

        private readonly Buffer<DiagramElement> goForwardBuffer = new Buffer<DiagramElement>();

        private readonly Settings settings;

        private readonly IEventAggregator eventAggregator;

        private GraphViewModel currentGraph;

        private DiagramElement currentElement;

        private DiagramElement zoomedElement;

        private double height;

        private double width;

        [ImportingConstructor]
        public DiagramViewModel(
            Settings settings,
            IGraphViewModelFactory graphViewModelFactory,
            IEventAggregator eventAggregator)
        {
            Contract.Requires(settings != null);
            Contract.Requires(graphViewModelFactory != null);
            Contract.Requires(eventAggregator != null);

            eventAggregator.Subscribe(this);
            this.settings = settings;
            this.eventAggregator = eventAggregator;
            this.graphViewModelFactory = graphViewModelFactory;
        }

        public Diagram Diagram { get; private set; }

        public DiagramElement CurrentElement
        {
            get { return this.currentElement; }
            private set
            {
                this.currentElement = value;
                this.NotifyOfPropertyChange(() => this.CurrentElement);
            }
        }

        public GraphViewModel CurrentGraph
        {
            get
            {
                return this.currentGraph;
            }
            set
            {
                if (value != null)
                {
                    this.currentGraph = value;
                    if (this.settings.UseGraphRatio)
                    {
                        this.currentGraph.Ratio = this.height / this.width;
                    }
                }

                this.NotifyOfPropertyChange(() => this.CurrentGraph);
            }
        }

        public double Width
        {
            set
            {
                this.width = value;
                this.CurrentGraph = this.currentGraph; // update current graph
            }
        }


        public double Height
        {
            set
            {
                this.height = value;
                this.CurrentGraph = this.currentGraph; // update current graph
            }
        }

        #region Messages Handlers

        public void Handle(ZoomToDiagramElementMessage message)
        {
            this.ZoomTo(message.DiagramElement);
            if (message.DiagramElement != this.currentElement)
            {
                this.CurrentElement = message.DiagramElement;
                this.CurrentDiagramElementSelected();                
            }
        }

        public void Handle(ZoomToCurrentElementMessage message)
        {
            this.ZoomTo(this.CurrentElement);
        }

        public void Handle(SelectDiagramElementMessage message)
        {
            this.CurrentElement = message.DiagramElement;
            this.CurrentDiagramElementSelected();
        }

        public void Handle(StartNewDiagramMessage message)
        {
            this.Diagram = new Diagram();
            this.PublishDiagramUpdatedMessage();
            this.CurrentElement = null;
            this.ZoomToImpl(this.Diagram);
            this.goBackBuffer.Clear();
            this.goForwardBuffer.Clear();
        }

        public void Handle(ZoomOutMessage message)
        {
            if (this.zoomedElement != null && this.zoomedElement.Parent != null)
            {
                this.ZoomTo(this.zoomedElement.Parent);
            }
        }

        public void Handle(ZoomToTopLevelMessage message)
        {
            // ZoomToTopLevel is pushlished after loading new assembly
            // If the zoomedElement was top-level diagram, we want it to be 
            // rendered again.
            this.zoomedElement = null;
            this.ZoomTo(this.Diagram);
        }

        public void Handle(DiagramUpdatedMessage message)
        {
            this.Diagram = message.Diagram;
        }

        public void Handle(GoBackMessage message)
        {
            if (this.goBackBuffer.IsEmpty)
            {
                return;
            }

            this.goBackBuffer.Pop();    // pop the current element, we want the one before...
            if (this.goBackBuffer.IsEmpty)
            {
                return;
            }

            var element = this.goBackBuffer.Pop();
            this.goForwardBuffer.Push(this.CurrentElement);
            this.ZoomTo(element);
        }

        public void Handle(GoForwardMessage message)
        {
            if (this.goForwardBuffer.IsEmpty)
            {
                return;
            }

            var element = this.goForwardBuffer.Pop();
            this.ZoomTo(element);
        }

        #endregion

        private void PublishDiagramUpdatedMessage()
        {
            this.eventAggregator.Publish(new DiagramUpdatedMessage(this.Diagram));
        }

        private void CurrentDiagramElementSelected()
        {
            this.eventAggregator.Publish(new DiagramElementSelectedMessage(this.CurrentElement));            
        }

        private void ZoomTo(DiagramElement diagramElement)
        {
            Contract.Requires(diagramElement != null);
            if (this.zoomedElement == diagramElement)
            {
                return;
            }

            this.goBackBuffer.Push(diagramElement);
            this.ZoomToImpl((dynamic)diagramElement);
            this.CurrentElement = diagramElement;
            this.CurrentDiagramElementSelected();
            this.zoomedElement = diagramElement;
        }

        private void ZoomToImpl(DiagramElement diagramElement)
        {
            Contract.Requires(diagramElement != null);
            Contract.Assert(false, string.Format("ZoomToImpl called with unexpected element with type {0}.", diagramElement.GetType()));
        }

        private void ZoomToImpl(Diagram diagram)
        {
            this.CurrentGraph = this.graphViewModelFactory.CreateGraphForDiagram(this, diagram);
        }

        private void ZoomToImpl(Layer layer)
        {
            this.CurrentGraph = this.graphViewModelFactory.CreateGraphForLayer(this, layer);
        }

        private void ZoomToImpl(Package package)
        {
            this.CurrentGraph = this.graphViewModelFactory.CreateGraphForPackage(this, package);
        }

        private void ZoomToImpl(TypeInfo type)
        {
            this.CurrentGraph = this.graphViewModelFactory.CreateGraphForType(this, type);
        }
    }
}
